{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "e1a45c5f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# # 安装依赖（在有网络的环境中执行）\n",
    "import sys\n",
    "# !{sys.executable} -m pip install -q agno requests python-dotenv"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e78b99d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os, re, json, math, time\n",
    "from typing import Optional, Dict, Any\n",
    "import requests\n",
    "\n",
    "# Open-Meteo 地理编码与天气接口（无需 API Key）\n",
    "GEOCODING_URL = 'https://geocoding-api.open-meteo.com/v1/search'\n",
    "FORECAST_URL  = 'https://api.open-meteo.com/v1/forecast'\n",
    "\n",
    "def geocode_city_http(name: str) -> Optional[Dict[str, Any]]:\n",
    "    try:\n",
    "        resp = requests.get(GEOCODING_URL, params={\n",
    "            'name': name, 'count': 1, 'language': 'zh', 'format': 'json'\n",
    "        }, timeout=10)\n",
    "        resp.raise_for_status()\n",
    "        data = resp.json() or {}\n",
    "        results = data.get('results') or []\n",
    "        if not results:\n",
    "            return None\n",
    "        r0 = results[0]\n",
    "        return {\n",
    "            'name': r0.get('name'),\n",
    "            'latitude': r0.get('latitude'),\n",
    "            'longitude': r0.get('longitude'),\n",
    "            'country': r0.get('country'),\n",
    "            'timezone': r0.get('timezone')\n",
    "        }\n",
    "    except Exception as e:\n",
    "        print('[Geocode] error:', e)\n",
    "        return None\n",
    "\n",
    "def get_current_weather_http(lat: float, lon: float, tz: Optional[str] = None) -> Optional[Dict[str, Any]]:\n",
    "    params = {\n",
    "        'latitude': lat,\n",
    "        'longitude': lon,\n",
    "        'current': 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m'\n",
    "    }\n",
    "    if tz:\n",
    "        params['timezone'] = tz\n",
    "    try:\n",
    "        resp = requests.get(FORECAST_URL, params=params, timeout=10)\n",
    "        resp.raise_for_status()\n",
    "        data = resp.json() or {}\n",
    "        cur = data.get('current') or {}\n",
    "        return cur\n",
    "    except Exception as e:\n",
    "        print('[Weather] error:', e)\n",
    "        return None\n",
    "\n",
    "# 简单 Weather Code 映射（示例，非完整）\n",
    "WEATHER_CODE_MAP = {\n",
    "    0: '晴', 1: '多云', 2: '阴', 3: '阴',\n",
    "    51: '毛毛雨', 61: '小雨', 63: '中雨', 65: '大雨',\n",
    "    71: '小雪', 73: '中雪', 75: '大雪', 95: '雷阵雨'\n",
    "}\n",
    "\n",
    "def pretty_weather(cur: Dict[str, Any]) -> str:\n",
    "    if not cur:\n",
    "        return '无法获取当前天气'\n",
    "    t = cur.get('temperature_2m')\n",
    "    rh = cur.get('relative_humidity_2m')\n",
    "    at = cur.get('apparent_temperature')\n",
    "    ws = cur.get('wind_speed_10m')\n",
    "    code = cur.get('weather_code')\n",
    "    desc = WEATHER_CODE_MAP.get(code, f'天气代码 {code}')\n",
    "    return f\"温度: {t}°C, 体感: {at}°C, 湿度: {rh}%, 风速: {ws} m/s, 天气: {desc}\"\n",
    "\n",
    "print(geocode_city_http(\"北京\"))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "7e9c4a3a",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "c:\\ProgramData\\miniconda3\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Using local HF model at: E:/huggingface_models/qwen/Qwen3-1.7B\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Loading checkpoint shards: 100%|██████████| 2/2 [00:01<00:00,  1.39it/s]\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "import torch\n",
    "from transformers import AutoTokenizer, AutoModelForCausalLM\n",
    "\n",
    "# 本地 HuggingFace 模型路径（可通过环境变量 QWEN_LOCAL_PATH 覆盖）\n",
    "LOCAL_QWEN_PATH = os.getenv('QWEN_LOCAL_PATH', 'E:/huggingface_models/qwen/Qwen3-1.7B')\n",
    "print('Using local HF model at:', LOCAL_QWEN_PATH)\n",
    "\n",
    "_device = 'cuda' if torch.cuda.is_available() else 'cpu'\n",
    "_tokenizer = AutoTokenizer.from_pretrained(LOCAL_QWEN_PATH, use_fast=True, trust_remote_code=True)\n",
    "_model = AutoModelForCausalLM.from_pretrained(LOCAL_QWEN_PATH, trust_remote_code=True).to(_device).eval()\n",
    "if _tokenizer.pad_token_id is None and _tokenizer.eos_token_id is not None:\n",
    "    _tokenizer.pad_token_id = _tokenizer.eos_token_id\n",
    "\n",
    "# 聊天模板与一次性生成\n",
    "\n",
    "def hf_build_prompt(messages):\n",
    "    if hasattr(_tokenizer, 'apply_chat_template'):\n",
    "        return _tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n",
    "    parts = []\n",
    "    for m in messages:\n",
    "        parts.append(f\"{m.get('role','user')}: {m.get('content','')}\")\n",
    "    parts.append('assistant:')\n",
    "    return '\\n'.join(parts)\n",
    "\n",
    "\n",
    "def hf_generate_once(prompt: str, max_new_tokens: int = 256, temperature: float = 0.3, top_p: float = 0.95) -> str:\n",
    "    inputs = _tokenizer(prompt, return_tensors='pt').to(_device)\n",
    "    # 推理不需要计算梯度，关闭梯度计算以节省显存，加快速度计算\n",
    "    with torch.no_grad():\n",
    "        output_ids = _model.generate(\n",
    "            **inputs,\n",
    "            max_new_tokens=max_new_tokens,\n",
    "            do_sample=(temperature > 0),\n",
    "            temperature=temperature,\n",
    "            top_p=top_p,\n",
    "            pad_token_id=_tokenizer.pad_token_id,\n",
    "        )\n",
    "    gen_ids = output_ids[0][inputs['input_ids'].shape[1]:]\n",
    "    return _tokenizer.decode(gen_ids, skip_special_tokens=True).strip()\n",
    "\n",
    "\n",
    "def summarize_weather_with_llm(city: str, loc: dict, cur: dict, user_query: str = '') -> str:\n",
    "    # 构造一个简短的上下文，要求中文优先（若用户为英文则英文优先）\n",
    "    system = (\n",
    "        'You are a helpful weather assistant. Answer in Chinese for Chinese queries; '\n",
    "        'otherwise answer in the user\\'s language. Be concise and factual.'\n",
    "    )\n",
    "    content = (\n",
    "        f\"用户查询: {user_query or city}\\n\"\n",
    "        f\"地点: {loc.get('name')} ({loc.get('country')})\\n\"\n",
    "        f\"坐标: {loc.get('latitude')}, {loc.get('longitude')}  时区: {loc.get('timezone')}\\n\"\n",
    "        f\"当前: 温度 {cur.get('temperature_2m')}°C, 体感 {cur.get('apparent_temperature')}°C, \"\n",
    "        f\"湿度 {cur.get('relative_humidity_2m')}%, 风速 {cur.get('wind_speed_10m')} m/s, 代码 {cur.get('weather_code')}\\n\"\n",
    "        '请生成一段友好的天气描述，并给出简短的出行建议。'\n",
    "    )\n",
    "    messages = [\n",
    "        { 'role': 'system', 'content': system },\n",
    "        { 'role': 'user', 'content': content },\n",
    "    ]\n",
    "    prompt = hf_build_prompt(messages)\n",
    "    return hf_generate_once(prompt, max_new_tokens=256, temperature=0.2, top_p=0.9)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "51845e0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 使用 agno 定义工具与 Agent\n",
    "try:\n",
    "    from agno.tools import tool\n",
    "    HAVE_AGNO = True\n",
    "except Exception as e:\n",
    "    HAVE_AGNO = False\n",
    "    print('Agno 未安装或导入失败：', e)\n",
    "\n",
    "if HAVE_AGNO:\n",
    "    @tool\n",
    "    def geocode_city(name: str) -> dict:\n",
    "        \"\"\"将城市名称解析为经纬度与时区信息。\n",
    "        返回: {name, latitude, longitude, country, timezone}\n",
    "        \"\"\"\n",
    "        r = geocode_city_http(name)\n",
    "        if not r:\n",
    "            return {'error': f'未找到城市: {name}'}\n",
    "        return r\n",
    "\n",
    "    @tool\n",
    "    def get_current_weather(lat: float, lon: float, timezone: str = '') -> dict:\n",
    "        \"\"\"根据经纬度（与可选时区）获取当前天气\n",
    "        返回: Open-Meteo current 字段的字典\n",
    "        \"\"\"\n",
    "        cur = get_current_weather_http(lat, lon, timezone or None)\n",
    "        return cur or {'error': '无法获取天气'}\n",
    "\n",
    "    @tool\n",
    "    def weather_by_city(name: str) -> dict:\n",
    "        \"\"\"便捷方法：直接传城市名，内部完成地理编码+天气查询\n",
    "        返回: { location: {...}, current: {...}, pretty: '...' }\n",
    "        \"\"\"\n",
    "        loc = geocode_city_http(name)\n",
    "        if not loc:\n",
    "            return {'error': f'未找到城市: {name}'}\n",
    "        cur = get_current_weather_http(loc['latitude'], loc['longitude'], loc.get('timezone'))\n",
    "        return {\n",
    "            'location': loc,\n",
    "            'current': cur,\n",
    "            'pretty': pretty_weather(cur or {})\n",
    "        }\n",
    "\n",
    "# 使用本地 Qwen3-1.7B 的简易 Agent，实现查询→调用 HTTP 工具→用本地 LLM 生成自然语言回复\n",
    "class LocalQwenWeatherAgent:\n",
    "    def run(self, text: str):\n",
    "        # 提取城市名（中文/英文）\n",
    "        m = re.search(r'(?:查询|查一下)?\\s*([\\u4e00-\\u9fa5A-Za-z\\s]+)\\s*(?:天气)', text)\n",
    "        if not m:\n",
    "            m = re.search(r'weather\\s*(?:in)?\\s*([A-Za-z\\u4e00-\\u9fa5\\s]+)', text, re.I)\n",
    "        city = (m.group(1).strip() if m else text.strip()) or 'Beijing'\n",
    "        loc = geocode_city_http(city)\n",
    "        if not loc:\n",
    "            return {'error': f'未找到城市: {city}'}\n",
    "        cur = get_current_weather_http(loc['latitude'], loc['longitude'], loc.get('timezone'))\n",
    "        pretty = pretty_weather(cur or {})\n",
    "        reply = summarize_weather_with_llm(city, loc, cur or {}, text)\n",
    "        return {\n",
    "            'location': loc,\n",
    "            'current': cur,\n",
    "            'pretty': pretty,\n",
    "            'reply': reply\n",
    "        }\n",
    "    def print_response(self, text: str):\n",
    "        res = self.run(text)\n",
    "        if isinstance(res, dict) and 'reply' in res:\n",
    "            print(res['reply'])\n",
    "        else:\n",
    "            print(json.dumps(res, ensure_ascii=False, indent=2))\n",
    "\n",
    "weather_agent = LocalQwenWeatherAgent()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "52b9cffa",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<think>\n",
      "好的，用户让我生成一段友好的天气描述和简短的出行建议。首先，我需要确认用户提供的数据是否正确。北京当前温度是9.8°C，体感7.5°C，湿度63%，风速4.6 m/s，时区是Asia/Shanghai。这些数据看起来没问题。\n",
      "\n",
      "接下来，我需要将这些数据转化为友好的天气描述。要突出温度和体感的差异，说明天气舒适，但可能有微风。湿度63%不算太高，可能不会有太大的闷热感。风速4.6 m/s属于轻风，适合户外活动。\n",
      "\n",
      "然后是出行建议。考虑到温度适宜，适合外出活动，但要注意防风和保暖。可能建议穿轻便外套，注意保暖，尤其是早晚温差。另外，湿度63%可能需要注意补水，尤其是在户外活动时。\n",
      "\n",
      "需要确保语言简洁，符合用户要求的简短。避免使用专业术语，保持口语化。同时，要检查是否有遗漏的信息，比如是否需要提醒带伞或者注意交通安全，但根据提供的数据，可能不需要。最后，确保整体语气友好，让用户感到被关心。\n",
      "</think>\n",
      "\n",
      "当前北京天气晴朗，气温9.8°C，体\n",
      "{\n",
      "  \"error\": \"未找到城市: Paris today\"\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "# 试运行：中文与英文示例\n",
    "def demo(query: str):\n",
    "    if weather_agent is None:\n",
    "        print('Agent 不可用')\n",
    "        return\n",
    "    if hasattr(weather_agent, 'print_response'):\n",
    "        weather_agent.print_response(query)\n",
    "    elif hasattr(weather_agent, 'run'):\n",
    "        res = weather_agent.run(query)\n",
    "        print(json.dumps(res, ensure_ascii=False, indent=2))\n",
    "    else:\n",
    "        print('未知 Agent 类型')\n",
    "\n",
    "demo('查询北京天气')\n",
    "demo(\"What's the weather in Paris today?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "67ad9cd7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 尝试导入 Model 基类，以构造本地模型适配器\n",
    "import asyncio\n",
    "from typing import Any, AsyncGenerator, Iterator\n",
    "LocalModelBase = None\n",
    "if 'HAVE_AGNO' in globals() and HAVE_AGNO:\n",
    "    try:\n",
    "        from agno.models import Model as _AgnoModelBase\n",
    "        LocalModelBase = _AgnoModelBase\n",
    "    except Exception:\n",
    "        try:\n",
    "            from agno.models.base import Model as _AgnoModelBase\n",
    "            LocalModelBase = _AgnoModelBase\n",
    "        except Exception as e:\n",
    "            print('无法导入 Agno Model 基类：', e)\n",
    "\n",
    "class LocalHFQwenModel(LocalModelBase if LocalModelBase else object):\n",
    "    \"\"\"将本地 HF Qwen3-1.7B 适配为 Agno 的 Model。\n",
    "    - 兼容 run()/invoke()/__call__/invoke_stream/ainvoke/ainvoke_stream\n",
    "    - 接受 messages 或 prompt 参数\n",
    "    - 返回字符串回复；流式时以整段一次性返回\n",
    "    \"\"\"\n",
    "    def __init__(self, model_id: str = 'local-qwen3-1.7b', name: str = 'Local Qwen3-1.7B'):\n",
    "        self.id = model_id\n",
    "        self.name = name\n",
    "\n",
    "    def _gen(self, messages=None, prompt: str = None, **kwargs) -> str:\n",
    "        if messages is None and prompt is not None:\n",
    "            messages = [{'role': 'user', 'content': prompt}]\n",
    "        if messages is None:\n",
    "            messages = [{'role': 'user', 'content': ''}]\n",
    "        built = hf_build_prompt(messages)\n",
    "        max_tokens = int(kwargs.get('max_output_tokens', 512) or 512)\n",
    "        temperature = float(kwargs.get('temperature', 0.2))\n",
    "        top_p = float(kwargs.get('top_p', 0.9))\n",
    "        return hf_generate_once(built, max_new_tokens=max_tokens, temperature=temperature, top_p=top_p)\n",
    "\n",
    "    # --- Agno 同步接口 ---\n",
    "    def run(self, *args, **kwargs) -> str:\n",
    "        prompt = kwargs.pop('prompt', None)\n",
    "        messages = kwargs.pop('messages', None)\n",
    "        return self._gen(messages=messages, prompt=prompt, **kwargs)\n",
    "\n",
    "    def invoke(self, *args, **kwargs) -> str:\n",
    "        return self.run(*args, **kwargs)\n",
    "\n",
    "    def invoke_stream(self, *args, **kwargs) -> Iterator[str]:\n",
    "        text = self.run(*args, **kwargs)\n",
    "        # 简化处理：一次性返回完整文本\n",
    "        yield text\n",
    "\n",
    "    # --- Agno 异步接口 ---\n",
    "    async def ainvoke(self, *args, **kwargs) -> str:\n",
    "        loop = asyncio.get_event_loop()\n",
    "        return await loop.run_in_executor(None, lambda: self.run(*args, **kwargs))\n",
    "\n",
    "    async def ainvoke_stream(self, *args, **kwargs) -> AsyncGenerator[str, None]:\n",
    "        text = await self.ainvoke(*args, **kwargs)\n",
    "        # 简化处理：一次性返回完整文本\n",
    "        yield text\n",
    "\n",
    "    # --- Provider 响应解析占位（返回原文以满足抽象接口） ---\n",
    "    def _parse_provider_response(self, raw: Any) -> Any:\n",
    "        return raw\n",
    "\n",
    "    def _parse_provider_response_delta(self, raw: Any) -> Any:\n",
    "        return raw\n",
    "\n",
    "    # 兜底可调用\n",
    "    def __call__(self, *args, **kwargs) -> str:\n",
    "        return self.run(*args, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "662db5a5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #ff8700; text-decoration-color: #ff8700\">╔═══════════════ AgentOS ════════════════╗</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>                                        <span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>                                        <span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>          <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">https://os.agno.com/</span>          <span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>                                        <span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>                                        <span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>  <span style=\"color: #ff8700; text-decoration-color: #ff8700; font-weight: bold\">OS running on:</span> http://localhost:7777  <span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>                                        <span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>                                        <span style=\"color: #ff8700; text-decoration-color: #ff8700\">║</span>\n",
       "<span style=\"color: #ff8700; text-decoration-color: #ff8700\">╚════════════════════════════════════════╝</span>\n",
       "</pre>\n"
      ],
      "text/plain": [
       "\u001b[38;5;208m╔═\u001b[0m\u001b[38;5;208m══════════════\u001b[0m\u001b[38;5;208m AgentOS \u001b[0m\u001b[38;5;208m═══════════════\u001b[0m\u001b[38;5;208m═╗\u001b[0m\n",
       "\u001b[38;5;208m║\u001b[0m                                        \u001b[38;5;208m║\u001b[0m\n",
       "\u001b[38;5;208m║\u001b[0m                                        \u001b[38;5;208m║\u001b[0m\n",
       "\u001b[38;5;208m║\u001b[0m          \u001b[1;36mhttps://os.agno.com/\u001b[0m          \u001b[38;5;208m║\u001b[0m\n",
       "\u001b[38;5;208m║\u001b[0m                                        \u001b[38;5;208m║\u001b[0m\n",
       "\u001b[38;5;208m║\u001b[0m                                        \u001b[38;5;208m║\u001b[0m\n",
       "\u001b[38;5;208m║\u001b[0m  \u001b[1;38;5;208mOS running on:\u001b[0m http://localhost:7777  \u001b[38;5;208m║\u001b[0m\n",
       "\u001b[38;5;208m║\u001b[0m                                        \u001b[38;5;208m║\u001b[0m\n",
       "\u001b[38;5;208m║\u001b[0m                                        \u001b[38;5;208m║\u001b[0m\n",
       "\u001b[38;5;208m╚════════════════════════════════════════╝\u001b[0m\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:     Will watch for changes in these directories: ['d:\\\\Workspace\\\\material-machine\\\\agent']\n",
      "INFO:     Uvicorn running on http://localhost:7777 (Press CTRL+C to quit)\n",
      "INFO:     Started reloader process [36280] using StatReload\n",
      "INFO:     Stopping reloader process [36280]\n"
     ]
    }
   ],
   "source": [
    "from agno.agent import Agent\n",
    "# pip install fastapi\n",
    "from agno.os import AgentOS\n",
    "# 创建多个Agentsearch_agent = Agent(    name=\"搜索助手\",    model=Claude(id=\"claude-sonnet-4-5\"),    db=SqliteDb(db_file=\"agents.db\"),    tools=[DuckDuckGoTools()],    add_history_to_context=True,    markdown=True)\n",
    "# from agno.models.ollama import Ollama\n",
    "# model=Ollama(id=\"llama3.1\"),\n",
    "local_model = LocalHFQwenModel()\n",
    "analyst_agent = Agent(name=\"数据分析师\", model=local_model,tools=[geocode_city,get_current_weather,weather_by_city], add_history_to_context=True,markdown=True)\n",
    "\n",
    "# 创建AgentOS实例\n",
    "agent_os = AgentOS(agents=[analyst_agent])\n",
    "# 获取FastAPI应用\n",
    "app = agent_os.get_app()\n",
    "\n",
    "\n",
    "# 启动服务\n",
    "if __name__ ==\"__main__\": \n",
    "    agent_os.serve(app=\"analyst_agent:app\", reload=True)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
